/*!
 * JDO - Javascript Data Objects
 *
 * Copyright 2013 Michael Coxon
 * Released under the GNU GPL v3 license
 *
 * Date: 2013-04-14
 */

(function(win) {

	// default constructor
	var jdo = function(){}
	
	/**
	 * Extends an object to be 'queryable' with the JDO functions (this is still provided for legacy support)
	 */
	function extend(obj) {
		if ( typeof obj == 'object' ) {
			var me = new jdo();
			
			for(var key in obj) {
				extend( obj[key] );
			}
		
			for(var key in me) {
				obj[key] = me[key];
			}	
		} 
	}
	
	function clone(obj) {
		if(obj == null || typeof(obj) != 'object') {
			return obj;
		}
		
		var newObj = new obj.constructor();
	
		for(var key in obj) {
			newObj[key] = clone(obj[key]);
		}
		
		return newObj;	
	}
	
	function gettype(obj) {
		if ( typeof(obj) != 'object' ) {
			return typeof(obj);
		}
		
		// null
		if ( obj === null ) {
			return 'null';
		}
		
		// jquery
		if ( typeof obj.jquery != 'undefined' ) {
			return 'jquery';
		}
		
		if ( typeof obj.constructor.name == 'undefined' ) {
			 obj.constructor.name = obj.constructor.toString().match(/^[\n\r\s]*function\s*([^\s(]+)/)[1];
		}
		return obj.constructor.name;
	}
	
	function getKey(key, rename) {
		if (rename === undefined) {
			rename = false;
		}
		
		var newKey = key;
		
		// key renaming
		var as = key.split(" as ");
		if ( as.length > 1 ) {
			key = as[0]
			newKey = as[1];
		}
		
		// no support for multi-word keys
		if ( key.indexOf(' ') > 0 || newKey.indexOf(' ') > 0 ) {
			throw new Error("Multi-word keys are not supported.");
		}
		
		if ( rename ) {
			return newKey;
		} else {
			return key;
		}
	}
	
	
	
	function SortByKey(arr, key, direction) {

		if (arr.length > 0) {
			switch ( typeof(arr[0][key]) ) {
				case 'string':
					arr.sort(function(a, b) { 
						return ((a[key] < b[key]) ? -1 : ((a[key] > b[key]) ? 1 : 0)) 
					});
					break;
					
				case 'number':
					arr.sort(function(a, b) { 
						return a[key] - b[key] 
					});
					break;
					
				case 'object':
					arr.sort(function(a, b) { 
						return ((a[key].toString() < b[key].toString()) ? -1 : ((a[key].toString() > b[key].toString()) ? 1 : 0)) 
					});
					break;
					
				default:
					throw new Error("Cannot sort type of '" + gettype(arr[0][key]) + "'.");
			}
			
			
			if (direction.toUpperCase() == 'DESC') {
				arr.reverse();
			}
		}
		
		return arr;
	}

	/**
	 * USAGE: SelectByArrayOfKeys(obj, ['key1','key2','key3']);
	 */
	function SelectByArrayOfKeys(obj, keys) {
		var output = new obj.constructor();
		
		extend( output );
		
		for(var i = 0; i < keys.length; i++) {
			
			var key = getKey(keys[i]);
			var newKey = getKey(keys[i], true);
						
			if ( typeof(obj[key]) != 'undefined' ) {
				output[newKey] = obj[key];
			} else {
				throw new Error("Key '"+ key +"' does not exist.");
			}
		}
		
		return output;	
	}
	
	// static
	win.Queryable = extend;
	
	/**
	 * USAGE: obj.Select(['key1','key2','key3']);
	 */
	jdo.prototype.Select = function(args) {
		if (gettype(this).toLowerCase() == 'array') {
			var output = [];
			
			extend( output );
			
			for( var i = 0; i < this.length; i++ ) {
				output[i] = this[i].Select(args);
			}
			
			
			return output;
			
		} else if ( typeof(this) == 'object' ) {
			switch ( gettype(args).toLowerCase() ) {
				case 'array':
					return SelectByArrayOfKeys(this, args);
					
				case 'string':	
					if ( args == "*" ) {
						throw new Error("Select type not required.");
					}
					return SelectByArrayOfKeys(this, [args]);
					
				default:
					throw new Error("Select type not supported.");
			}	
		} else {
			throw new Error("Can only Select on objects and arrays of objects");
		}
	}
	
	/**
	 * Returns the objects that evaluate true on the provided comparer function.
	 *
	 * USAGE: obj.Where(function() { return true; });
	 */
	jdo.prototype.Where = function(comparer) {
		if (gettype(this).toLowerCase() == 'array') {
			var output = [];
			
			extend( output );
			
			var oi = 0;
			
			for( var i = 0; i < this.length; i++ ) {
				var ret = this[i].Where(comparer);
				if ( ret != null ) {
					output[oi] = ret;
					oi++;
				} 
			}
			
			
			return output;
			
		} else if ( typeof(this) == 'object' ) {
			if ( comparer.call(this) ) {
				return this;
			} 
			
			return null;
			
		} else {
			throw new Error("Can only Where on objects and arrays of objects");
		}
	}
	
	/**
     * Orders the set by specified keys where the first orderby param is first preference
	 *
	 * USAGE: obj.OrderBy(['key1 DESC','key2','key3 ASC']);
	 */
	jdo.prototype.OrderBy = function(args) {
		if (gettype(this).toLowerCase() == 'array') {
			var output = this;
			
			switch ( gettype(args).toLowerCase() ) {
				case 'array':
					for( var o=args.length-1; o != -1; o-- ) {
						if ( typeof args[o] != 'string' ) throw new Error("Each item must be a string.");
						
						var sortby = args[o].split(' ');
						var key = sortby[0];
						var direction = typeof(sortby[1]) !== 'undefined' ? sortby[1] : 'ASC';
						
						output = SortByKey(output, key, direction);
					}
					break;
					
				default:
					throw new Error("OrderBy type not supported.")
			}
			
			return output;
			
		} else {
			throw new Error("Can only OrderBy on arrays of objects");
		}
	}
	
	
	/**
     * Returns a segment of rows.
	 *
	 * USAGE: obj.Limit(5,100); // return 100 rows from the 5th item
	 * USAGE: obj.Limit(100); // return the first 100 rows;
	 */
	jdo.prototype.Limit = function(startat, rowcount) {
		if ( rowcount === undefined ) {
			rowcount = startat;
			startat = 1;
		}
		
		if (gettype(this).toLowerCase() == 'array') {
			var output = [];
			
			extend( output );
			
			// decrement so it is zero based
			startat--;
			
			for( var i = 0; i + startat < this.length && i < rowcount; i++ ) {
				output[i] = this[i + startat];
			}
			
			
			return output;
			
		} else if ( typeof(this) == 'object' ) {
			return this;
		} else {
			throw new Error("Can only Limit on arrays of objects");
		}
	}
	
	
	
	/**
	 * Returns a copy of the object
	 */
	jdo.prototype.Clone = function() {
		return clone(this);
	};
	
})(window);